如何利用Runtime构造payload
Java安全[反射(2)]
第一篇讲到过,如果想要加载一个类,可以同forName
进行加载,但是正常情况下我们一般用到的是import
,所以forName
就可以帮助攻击者加载任意类。
内部类
对于$
,在很多源码里看到,类名包含$
符号,比如在fastjion
在checkAutoType
时候就会先将 $
替换为.
可以看到类名的$
被替换为.
来解析,所以$
起的作用实际就是查找内部类。
写个例子,在一个普通类My
中,写一个内部类Your
,然后编译看看output
文件夹会生成什么
1 | package reflect2; |
可以看到,Your
类对应生成了一个My$Your.class
,My
类对应生成了一个My.class
。
我们还可以试试加载这两个,看看是否有区别
1 | package reflect2; |
发现初始化内部类时,外部类并没有被初始化,所以在一定程度上可以将它们当作两个无关类。
根据上面所说,Java会将$
当作 .
,那如果直接把$
换成 .
的话会怎么样
发现运行报错,原因是Java编译器有自己的规则,$
在它的规则中是外部类和内部类的分隔符,但是如果用 .
来分割外部类和内部类就会让其分不清意图,从而报错,虽然其内部会将其当作 .
,但是前提还是$
被当作内外部类分割符后处理。
getRuntime
class.newInstrance()
作用是调用这个类中的无参构造函数,但是经常直接在payload
中调用newIntstrance
时往往会报错,主要有两个原因
- 目标类没有无参构造函数
- 目标类的构造函数是私有的
最常遇到的情况下是,调用java.lang.Runtime
,
1 | package reflect2; |
发现报错中提示java.lang.Runtime
是一个私有的类,是无法直接调用其中的方法
继续跟进这个类的内容,
Runtime
确实为了安全考虑,将其的构造函数设置为私有,为了不让任何其他人实例化这个类,这里的话就无法通过newInstrance
直接进行实例化Runtime
,也就无法执行exec
函数。
那这里就会有个问题,如果有类的构造函数是私有,那不是代表当用户想要使用这个类时,无法进行实例化,就无法使用,而正常业务中为什么会出现这种情况呢?
其实,这种情况叫做“单例模式”
,是一种很常见的业务模式。
比如,网站的数据库连接,当连接成功后,就不需要每用一次就建立一次网站数据库连接,这样就会建立多个数据库连接,造成资源浪费。这样开发者在编写代码时就会将构造函数写出私有,并通过静态方法
来获取这个函数。
在第一篇中提到过,初始化时,静态方法和静态变量
只加载一次
,而创建类对象时,构造函数则会每构造一个类对象就执行一次。
写个代码举个例子,
1 | package reflect2; |
这样只有在初始化时才会执行一次静态变量,实例化TrainDB
类,并执行构造函数,而后只能通过调用getInstance()
,才可以获得其实例,但是不会执行构造函数,这样也避免了多次建立实例。
paload构造
回归正题,Runtime
也是一样的,它也是单例模式,只能通过Runtime.getRuntime()
获取Runtime
的实例
那么要构造payload
就需要改一下,就不能用newIntstrance
进行对Runtime
的实例化,只有通过Runtime.getRuntime
这个设定的静态方法获取Runtime
的实例化后的对象。
1 | package reflect2; |
在这里,
1 | Class clazz = Class.forName("java.lang.Runtime"); |
发现用了getMethod
方法和invoke
方法,
getMethod
getMethod
的作用就算通过反射获得一个类的某个特定的公有方法。其需要两个参数,第一个是方法名,第二个是方法所需参数的类型 [ 比如,字符串就算String.class
] 。
但是在Java中支持类的重载,也就是可能存在多个相同的名字
的但是参数列表或者类型不同的方法,所以只知道名字并不能直接确认函数。
所以在这里想调用exec
方法时,就需要看看在Runtime
中其重载列表,看看目标方法中所需的参数类型及其列表。
这里可以用第四个重载类型,只要一个字符串,最简单。而前三个要字符串数组,也就是一个命令加上参数之类的。
所以就得到了通过以下代码获取Runtime.exec
方法
1 | getMethod("exec",String.class); |
getMethod
获得这个方法后,就需要执行这个方法,比如传入参数等等。
invoke
invoke
的作用就是执行方法,它的第一个参数是:
如果这个方法是一个普通的方法,那么第一个参数就是类对象
如果这个方法是一个静态的方法,那么第一个参数是类
原因是普通方法需要类实例化后得到类对象,才可以调用该普通方法,所以需要传入类对象。
而静态方法不用实例化类,就可以直接调用,所以传入类名即可。
其实转化一下就更加清楚了,
正常调用一个方法是 [1].method([2], [3], [4]...)
,而在反射里就是 method.invoke([1], [2], [3], [4]...)
。其中[1]
是类或者类对象,而后[...]
就是传入方法的参数。
paload分析
按上述的,分解一下payload
,
这里先初始化Runtime
类,
然后获取Runtime
的exec
方法,
然后再获取Runtime
的getRuntime
方法,
然后执行getRuntime
获取Runtime
的实例化对象,这里invoke
传入任何都可以,因为这里getRuntime
方法是无参方法,所以不需要参数也行。
最后调用,exec
方法,invoke
第一个传入Runtime
的实例化对象,第二传入执行的命令calc
。
1 | Class clazz = Class.forName("java.lang.Runtime"); |
最后两个疑问,
- 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
- 如果一个方法或构造方法是私有方法,我们是否能执行它呢?